﻿using System.Collections.Generic;
using System.Linq;
using PostSharp.CodeWeaver;
using PostSharp.CodeModel;
using PostSharp.CodeModel.Helpers;
using PostSharp.Collections;
using System.Reflection;

namespace PostSharp4ViewState.Weaver
{
   sealed class SaveAdvice : PersistAdvice
   {
      public SaveAdvice(PersistTask parent, PersistMode mode, PerTypeData data)
         : base(parent, mode, mode == PersistMode.ViewState ? "SaveViewState" : "SaveControlState", data)
      {         
      }

      public override JoinPointKinds JointPointKind
      {
         get { return JoinPointKinds.AfterMethodBodySuccess; }
      }
      protected override void DoCreateMethod(MethodDefDeclaration methodDef)
      {
         methodDef.Name = _methodName;
         methodDef.Attributes = MethodAttributes.Virtual | MethodAttributes.Family | MethodAttributes.HideBySig;
         methodDef.CallingConvention = CallingConvention.HasThis;         

         methodDef.ReturnParameter = new ParameterDeclaration();
         methodDef.ReturnParameter.Attributes = ParameterAttributes.Retval;
         methodDef.ReturnParameter.ParameterType = _parent.ObjectType;
         MethodBodyDeclaration methodBody = new MethodBodyDeclaration();
         methodDef.MethodBody = methodBody;
         methodBody.RootInstructionBlock = methodBody.CreateInstructionBlock();
      }

      private void WeavePrivate(MethodDefDeclaration wovenMethod, InstructionWriter writer, InstructionBlock block, LocalVariableSymbol returnVar)
      {
         IEnumerable<FieldDefDeclaration> fields = _data.GetMembers<FieldDefDeclaration>(_mode);
         IEnumerable<PropertyDeclaration> properties = _data.GetMembers<PropertyDeclaration>(_mode);

         InstructionSequence seqBefore = wovenMethod.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seqBefore, NodePosition.Before, null);
         writer.AttachInstructionSequence(seqBefore);
         writer.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

         LocalVariableSymbol tmpVar = block.DefineLocalVariable(_parent.ObjectArrayType, "~tmp~{0}");
         LocalVariableSymbol tmpOldValue = block.DefineLocalVariable(_parent.ObjectType, "~tmpOldValue~{0}");
         LocalVariableSymbol tmpResultTyped = block.DefineLocalVariable(_parent.ObjectArrayType, "~tmpResultTyped~{0}");         

         if (returnVar != null)
         {
            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, returnVar);            
         }
         else
         {
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            IMethod baseMethod = GetInheritedMethod(wovenMethod.DeclaringType.BaseType.GetTypeDefinition(), _methodName, writer.MethodBody.Module);            
            writer.EmitInstructionMethod(OpCodeNumber.Call, baseMethod);            
         }

         writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpOldValue);

         writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, fields.Count() + properties.Count());
         writer.EmitInstructionType(OpCodeNumber.Newarr, _parent.ObjectType);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpVar);

         int i = 0;
         foreach (FieldDefDeclaration fieldDef in fields)
         {            
            writer.EmitInstruction(OpCodeNumber.Nop);

            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            writer.EmitInstructionField(OpCodeNumber.Ldfld, GenericHelper.GetFieldCanonicalGenericInstance(fieldDef));
            if (fieldDef.FieldType.GetSystemType(null, null).IsValueType)
            {
               writer.EmitInstructionType(OpCodeNumber.Box, fieldDef.FieldType);
            }
            writer.EmitInstruction(OpCodeNumber.Stelem_Ref);
            i++;
         }
         foreach (PropertyDeclaration prop in properties)
         {
            writer.EmitInstruction(OpCodeNumber.Nop);

            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            writer.EmitInstructionMethod(OpCodeNumber.Call, GenericHelper.GetMethodCanonicalGenericInstance(prop.Members.GetBySemantic(MethodSemantics.Getter).Method));            
            if (prop.PropertyType.GetSystemType(null, null).IsValueType)
            {
               writer.EmitInstructionType(OpCodeNumber.Box, prop.PropertyType);
            }
            writer.EmitInstruction(OpCodeNumber.Stelem_Ref);
            i++;
         }

         writer.EmitInstruction(OpCodeNumber.Nop);

         writer.EmitInstruction(OpCodeNumber.Ldc_I4_2);
         writer.EmitInstructionType(OpCodeNumber.Newarr, _parent.ObjectType);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpResultTyped);

         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpResultTyped);
         writer.EmitInstruction(OpCodeNumber.Ldc_I4_0);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpOldValue);
         writer.EmitInstruction(OpCodeNumber.Stelem_Ref);

         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpResultTyped);
         writer.EmitInstruction(OpCodeNumber.Ldc_I4_1);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar);
         writer.EmitInstruction(OpCodeNumber.Stelem_Ref);
         
         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpResultTyped);
         if (returnVar != null)
         {
            writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, returnVar);
         }
         else
         {
            writer.EmitInstruction(OpCodeNumber.Ret);
         }
         writer.DetachInstructionSequence();
      }

      public override void Weave(WeavingContext context, InstructionBlock block)
      {
         if (context.Method.Name == _methodName)
         {
            MethodDefDeclaration wovenMethod = context.Method;
            WeavePrivate(wovenMethod, context.InstructionWriter, block, context.ReturnValueVariable);
         }
         else
         {
            MethodDefDeclaration wovenMethod = context.Method.DeclaringType.Methods.GetByName(_methodName).FirstOrDefault();
            WeavePrivate(wovenMethod, context.InstructionWriter, wovenMethod.MethodBody.RootInstructionBlock, null);
         }
      }  
   }
}
